/*
    SUSAN® - Sole of Unix Save ANything

   
      Copyright (C) 2011-2016 Skyatlas Co. LTD

   

   
*/
/*
 * Kern Sibbald, MM
 */
/**
 * @file
 * Second generation Storage daemon.
 *
 * It accepts a number of simple commands from the File daemon
 * and acts on them. When a request to append data is made,
 * it opens a data channel and accepts data from the
 * File daemon.
 */

#include "include/susan.h"
#include "stored/stored.h"
#include "lib/crypto_cache.h"
#include "stored/acquire.h"
#include "stored/autochanger.h"
#include "stored/bsr.h"
#include "stored/device.h"
#include "stored/jcr_private.h"
#include "stored/job.h"
#include "stored/label.h"
#include "stored/ndmp_tape.h"
#include "stored/sd_backends.h"
#include "stored/sd_device_control_record.h"
#include "stored/sd_stats.h"
#include "stored/socket_server.h"
#include "stored/stored_globals.h"
#include "stored/wait.h"
#include "lib/berrno.h"
#include "lib/bsock.h"
#include "lib/bnet_network_dump.h"
#include "lib/daemon.h"
#include "lib/bsignal.h"
#include "lib/parse_conf.h"
#include "lib/thread_specific_data.h"
#include "lib/util.h"
#include "lib/watchdog.h"
#include "include/jcr.h"


namespace storagedaemon {
extern bool ParseSdConfig(const char* configfile, int exit_code);
}

using namespace storagedaemon;

/* Imported functions */
extern bool PrintMessage(void* sock, const char* fmt, ...);

/* Forward referenced functions */
namespace storagedaemon {
#if !defined(HAVE_WIN32)
static
#endif
    void
    TerminateStored(int sig);
}  // namespace storagedaemon
static int CheckResources();
static void CleanUpOldFiles();

extern "C" void* device_initialization(void* arg);

/* Global static variables */
static bool foreground = 0;

static void usage()
{
  kSusanVersionStrings.PrintCopyrightWithFsfAndPlanets(stderr, 2000);
  fprintf(
      stderr,
      _("Usage: susan-sd [options]\n"
        "        -c <path>   use <path> as configuration file or directory\n"
        "        -d <nn>     set debug level to <nn>\n"
        "        -dt         print timestamp in debug output\n"
        "        -f          run in foreground (for debugging)\n"
        "        -g <group>  run as group <group>\n"
        "        -m          print kaboom output (for debugging)\n"
        "        -p          proceed despite I/O errors\n"
        "        -s          no signals (for debugging)\n"
        "        -t          test - read configuration and exit\n"
        "        -u <user>   run as user <user>\n"
        "        -v          verbose user messages\n"
        "        -xc         print configuration and exit\n"
        "        -xs         print configuration file schema in JSON format "
        "and exit\n"
        "        -?          print this message.\n"
        "\n"));
  exit(1);
}

/*********************************************************************
 *
 *  Main Susan Storage Daemon
 *
 */
#if defined(HAVE_WIN32)
#define main SusanMain
#endif

int main(int argc, char* argv[])
{
  int ch;
  bool no_signals = false;
  bool test_config = false;
  bool export_config = false;
  bool export_config_schema = false;
  pthread_t thid;
  char* uid = NULL;
  char* gid = NULL;

  setlocale(LC_ALL, "");
  tzset();
  bindtextdomain("susan", LOCALEDIR);
  textdomain("susan");

  InitStackDump();
  MyNameIs(argc, argv, "susan-sd");
  InitMsg(NULL, NULL);
  daemon_start_time = time(NULL);

  /*
   * Sanity checks
   */
  if (TAPE_BSIZE % B_DEV_BSIZE != 0 || TAPE_BSIZE / B_DEV_BSIZE == 0) {
    Emsg2(M_ABORT, 0,
          _("Tape block size (%d) not multiple of system size (%d)\n"),
          TAPE_BSIZE, B_DEV_BSIZE);
  }
  if (TAPE_BSIZE != (1 << (ffs(TAPE_BSIZE) - 1))) {
    Emsg1(M_ABORT, 0, _("Tape block size (%d) is not a power of 2\n"),
          TAPE_BSIZE);
  }

  while ((ch = getopt(argc, argv, "c:d:fg:mpstu:vx:z:?")) != -1) {
    switch (ch) {
      case 'c': /* configuration file */
        if (configfile != NULL) { free(configfile); }
        configfile = strdup(optarg);
        break;

      case 'd': /* debug level */
        if (*optarg == 't') {
          dbg_timestamp = true;
        } else {
          debug_level = atoi(optarg);
          if (debug_level <= 0) { debug_level = 1; }
        }
        break;

      case 'f': /* run in foreground */
        foreground = true;
        break;

      case 'g': /* set group id */
        gid = optarg;
        break;

      case 'm': /* print kaboom output */
        prt_kaboom = true;
        break;

      case 'p': /* proceed in spite of I/O errors */
        forge_on = true;
        break;

      case 's': /* no signals */
        no_signals = true;
        break;

      case 't':
        test_config = true;
        break;

      case 'u': /* set uid */
        uid = optarg;
        break;

      case 'v': /* verbose */
        verbose++;
        break;

      case 'x': /* export configuration/schema and exit */
        if (*optarg == 's') {
          export_config_schema = true;
        } else if (*optarg == 'c') {
          export_config = true;
        } else {
          usage();
        }
        break;

      case 'z': /* switch network debugging on */
        if (!BnetDump::EvaluateCommandLineArgs(optarg)) { exit(1); }
        break;

      case '?':
      default:
        usage();
        break;
    }
  }
  argc -= optind;
  argv += optind;

  if (!no_signals) { InitSignals(TerminateStored); }

  if (argc) {
    if (configfile != NULL) { free(configfile); }
    configfile = strdup(*argv);
    argc--;
    argv++;
  }
  if (argc) { usage(); }

  /*
   * See if we want to drop privs.
   */
  if (geteuid() == 0) { drop(uid, gid, false); }

  if (export_config_schema) {
    PoolMem buffer;

    my_config = InitSdConfig(configfile, M_ERROR_TERM);
    PrintConfigSchemaJson(buffer);
    printf("%s\n", buffer.c_str());
    goto bail_out;
  }

  my_config = InitSdConfig(configfile, M_ERROR_TERM);
  ParseSdConfig(configfile, M_ERROR_TERM);

  if (forge_on) {
    my_config->AddWarning(
        "Running with '-p' is for testing and emergency recovery purposes "
        "only");
  }

  if (export_config) {
    my_config->DumpResources(PrintMessage, NULL);
    goto bail_out;
  }

  if (!foreground && !test_config) {
    daemon_start();  /* become daemon */
    InitStackDump(); /* pick up new pid */
  }

  if (InitCrypto() != 0) {
    Jmsg((JobControlRecord*)NULL, M_ERROR_TERM, 0,
         _("Cryptography library initialization failed.\n"));
  }

  if (!CheckResources()) {
    Jmsg((JobControlRecord*)NULL, M_ERROR_TERM, 0,
         _("Please correct the configuration in %s\n"),
         my_config->get_base_config_path().c_str());
  }

  InitReservationsLock();

  if (test_config) {
    if (my_config->HasWarnings()) {
      /* messaging not initialized, so Jmsg with  M_WARNING doesn't work */
      fprintf(stderr, _("There are configuration warnings:\n"));
      for (auto& warning : my_config->GetWarnings()) {
        fprintf(stderr, " * %s\n", warning.c_str());
      }
    }
    TerminateStored(0);
  }

  MyNameIs(0, (char**)NULL, me->resource_name_); /* Set our real name */

  CreatePidFile(me->pid_directory, "susan-sd",
                GetFirstPortHostOrder(me->SDaddrs));
  ReadStateFile(me->working_directory, "susan-sd",
                GetFirstPortHostOrder(me->SDaddrs));
  ReadCryptoCache(me->working_directory, "susan-sd",
                  GetFirstPortHostOrder(me->SDaddrs));

  SetJcrInThreadSpecificData(nullptr);

  LoadSdPlugins(me->plugin_directory, me->plugin_names);

  CleanUpOldFiles();

  /* Ensure that Volume Session Time and Id are both
   * set and are both non-zero.
   */
  vol_session_time = (uint32_t)daemon_start_time;
  if (vol_session_time == 0) { /* paranoid */
    Jmsg0(NULL, M_ABORT, 0, _("Volume Session Time is ZERO!\n"));
  }

  /*
   * Start the device allocation thread
   */
  CreateVolumeLists(); /* do before device_init */
  if (pthread_create(&thid, NULL, device_initialization, NULL) != 0) {
    BErrNo be;
    Emsg1(M_ABORT, 0, _("Unable to create thread. ERR=%s\n"), be.bstrerror());
  }

  InitJcrChain();
  StartWatchdog(); /* start watchdog thread */
  if (me->jcr_watchdog_time) {
    InitJcrSubsystem(
        me->jcr_watchdog_time); /* start JobControlRecord watchdogs etc. */
  }

  StartStatisticsThread();

#if HAVE_NDMP
  /*
   * Separate thread that handles NDMP connections
   */
  if (me->ndmp_enable) {
    StartNdmpThreadServer(me->NDMPaddrs, me->MaxConnections);
  }
#endif

  /*
   * Single server used for Director/Storage and File daemon
   */
  StartSocketServer(me->SDaddrs);

  /* to keep compiler quiet */
  TerminateStored(0);

bail_out:
  return 0;
}

/* Check Configuration file for necessary info */
static int CheckResources()
{
  bool OK = true;
  const std::string& configfile = my_config->get_base_config_path();

  if (my_config->GetNextRes(R_STORAGE, (SusanResource*)me) != NULL) {
    Jmsg1(NULL, M_ERROR, 0, _("Only one Storage resource permitted in %s\n"),
          configfile.c_str());
    OK = false;
  }

  if (my_config->GetNextRes(R_DIRECTOR, NULL) == NULL) {
    Jmsg1(NULL, M_ERROR, 0,
          _("No Director resource defined in %s. Cannot continue.\n"),
          configfile.c_str());
    OK = false;
  }

  if (my_config->GetNextRes(R_DEVICE, NULL) == NULL) {
    Jmsg1(NULL, M_ERROR, 0,
          _("No Device resource defined in %s. Cannot continue.\n"),
          configfile.c_str());
    OK = false;
  }

  /*
   * Sanity check.
   */
  if (me->MaxConnections < ((2 * me->MaxConcurrentJobs) + 2)) {
    me->MaxConnections = (2 * me->MaxConcurrentJobs) + 2;
  }

  if (!me->messages) {
    me->messages = (MessagesResource*)my_config->GetNextRes(R_MSGS, NULL);
    if (!me->messages) {
      Jmsg1(NULL, M_ERROR, 0,
            _("No Messages resource defined in %s. Cannot continue.\n"),
            configfile.c_str());
      OK = false;
    }
  }

  if (!me->working_directory) {
    Jmsg1(NULL, M_ERROR, 0,
          _("No Working Directory defined in %s. Cannot continue.\n"),
          configfile.c_str());
    OK = false;
  }

  StorageResource* store = me;
  if (store->IsTlsConfigured()) {
    if (!have_tls) {
      Jmsg(NULL, M_FATAL, 0, _("TLS required but not compiled into Susan.\n"));
      OK = false;
    }
  }

  DeviceResource* device_resource = nullptr;
  foreach_res (device_resource, R_DEVICE) {
    if (device_resource->drive_crypto_enabled &&
        BitIsSet(CAP_LABEL, device_resource->cap_bits)) {
      Jmsg(NULL, M_FATAL, 0,
           _("LabelMedia enabled is incompatible with tape crypto on Device "
             "\"%s\" in %s.\n"),
           device_resource->resource_name_, configfile.c_str());
      OK = false;
    }
  }

  if (OK) { OK = InitAutochangers(); }

  if (OK) {
    CloseMsg(NULL);              /* close temp message handler */
    InitMsg(NULL, me->messages); /* open daemon message handler */
    SetWorkingDirectory(me->working_directory);
    if (me->secure_erase_cmdline) {
      SetSecureEraseCmdline(me->secure_erase_cmdline);
    }
    if (me->log_timestamp_format) {
      SetLogTimestampFormat(me->log_timestamp_format);
    }
  }

  return OK;
}

/**
 * Remove old .spool files written by me from the working directory.
 */
static void CleanUpOldFiles()
{
  DIR* dp;
  struct dirent* result;
#ifdef USE_READDIR_R
  struct dirent* entry;
#endif
  int rc, name_max;
  int my_name_len = strlen(my_name);
  int len = strlen(me->working_directory);
  POOLMEM* cleanup = GetPoolMemory(PM_MESSAGE);
  POOLMEM* basename = GetPoolMemory(PM_MESSAGE);
  regex_t preg1{};
  char prbuf[500];
  BErrNo be;

  /* Look for .spool files but don't allow spaces */
  const char* pat1 = "^[^ ]+\\.spool$";

  /* Setup working directory prefix */
  PmStrcpy(basename, me->working_directory);
  if (len > 0 && !IsPathSeparator(me->working_directory[len - 1])) {
    PmStrcat(basename, "/");
  }

  /* Compile regex expressions */
  rc = regcomp(&preg1, pat1, REG_EXTENDED);
  if (rc != 0) {
    regerror(rc, &preg1, prbuf, sizeof(prbuf));
    Pmsg2(000, _("Could not compile regex pattern \"%s\" ERR=%s\n"), pat1,
          prbuf);
    goto get_out2;
  }

  name_max = pathconf(".", _PC_NAME_MAX);
  if (name_max < 1024) { name_max = 1024; }

  if (!(dp = opendir(me->working_directory))) {
    BErrNo be;
    Pmsg2(000, "Failed to open working dir %s for cleanup: ERR=%s\n",
          me->working_directory, be.bstrerror());
    goto get_out1;
  }

#ifdef USE_READDIR_R
  entry = (struct dirent*)malloc(sizeof(struct dirent) + name_max + 1000);
  while (1) {
    if ((Readdir_r(dp, entry, &result) != 0) || (result == NULL)) {
#else
  while (1) {
    result = readdir(dp);
    if (result == NULL) {
#endif
      break;
    }

    /* Exclude any name with ., .., not my_name or containing a space */
    if (strcmp(result->d_name, ".") == 0 || strcmp(result->d_name, "..") == 0 ||
        strncmp(result->d_name, my_name, my_name_len) != 0) {
      Dmsg1(500, "Skipped: %s\n", result->d_name);
      continue;
    }

    /* Unlink files that match regex */
    if (regexec(&preg1, result->d_name, 0, NULL, 0) == 0) {
      PmStrcpy(cleanup, basename);
      PmStrcat(cleanup, result->d_name);
      Dmsg1(500, "Unlink: %s\n", cleanup);
      SecureErase(NULL, cleanup);
    }
  }
#ifdef USE_READDIR_R
  free(entry);
#endif
  closedir(dp);

get_out1:
  regfree(&preg1);
get_out2:
  FreePoolMemory(cleanup);
  FreePoolMemory(basename);
}


/**
 * Here we attempt to init and open each device. This is done once at startup in
 * a separate thread.
 */
extern "C" void* device_initialization(void* arg)
{
  DeviceResource* device_resource = nullptr;
  DeviceControlRecord* dcr;
  JobControlRecord* jcr;
  Device* dev;
  int errstat;

  LockRes(my_config);

  pthread_detach(pthread_self());
  jcr = NewStoredJcr();
  NewPlugins(jcr); /* instantiate plugins */
  jcr->setJobType(JT_SYSTEM);

  /*
   * Initialize job start condition variable
   */
  errstat = pthread_cond_init(&jcr->impl->job_start_wait, NULL);
  if (errstat != 0) {
    BErrNo be;
    Jmsg1(jcr, M_ABORT, 0,
          _("Unable to init job start cond variable: ERR=%s\n"),
          be.bstrerror(errstat));
  }

  /*
   * Initialize job end condition variable
   */
  errstat = pthread_cond_init(&jcr->impl->job_end_wait, NULL);
  if (errstat != 0) {
    BErrNo be;
    Jmsg1(jcr, M_ABORT, 0,
          _("Unable to init job endstart cond variable: ERR=%s\n"),
          be.bstrerror(errstat));
  }

  foreach_res (device_resource, R_DEVICE) {
    Dmsg1(90, "calling FactoryCreateDevice %s\n", device_resource->device_name);
    dev = FactoryCreateDevice(NULL, device_resource);
    Dmsg1(10, "SD init done %s\n", device_resource->device_name);
    if (!dev) {
      Jmsg1(NULL, M_ERROR, 0, _("Could not initialize %s\n"),
            device_resource->device_name);
      continue;
    }

    dcr = new StorageDaemonDeviceControlRecord;
    jcr->impl->dcr = dcr;
    SetupNewDcrDevice(jcr, dcr, dev, NULL);
    jcr->impl->dcr->SetWillWrite();
    GeneratePluginEvent(jcr, bSdEventDeviceInit, dcr);
    if (dev->IsAutochanger()) {
      /*
       * If autochanger set slot in dev structure
       */
      GetAutochangerLoadedSlot(dcr);
    }

    if (BitIsSet(CAP_ALWAYSOPEN, device_resource->cap_bits)) {
      Dmsg1(20, "calling FirstOpenDevice %s\n", dev->print_name());
      if (!FirstOpenDevice(dcr)) {
        Jmsg1(NULL, M_ERROR, 0, _("Could not open device %s\n"),
              dev->print_name());
        Dmsg1(20, "Could not open device %s\n", dev->print_name());
        FreeDeviceControlRecord(dcr);
        jcr->impl->dcr = NULL;
        continue;
      }
    }

    if (BitIsSet(CAP_AUTOMOUNT, device_resource->cap_bits) && dev->IsOpen()) {
      switch (ReadDevVolumeLabel(dcr)) {
        case VOL_OK:
          memcpy(&dev->VolCatInfo, &dcr->VolCatInfo, sizeof(dev->VolCatInfo));
          VolumeUnused(dcr); /* mark volume "released" */
          break;
        default:
          Jmsg1(NULL, M_WARNING, 0, _("Could not mount device %s\n"),
                dev->print_name());
          break;
      }
    }
    FreeDeviceControlRecord(dcr);
    jcr->impl->dcr = NULL;
  }
  FreeJcr(jcr);
  init_done = true;
  UnlockRes(my_config);
  return NULL;
}

/**
 * Clean up and then exit
 */
namespace storagedaemon {

#if !defined(HAVE_WIN32)
static
#endif
    void
    TerminateStored(int sig)
{
  static bool in_here = false;
  DeviceResource* device_resource = nullptr;
  JobControlRecord* jcr;

  if (in_here) {       /* prevent loops */
    Bmicrosleep(2, 0); /* yield */
    exit(1);
  }
  in_here = true;
  debug_level = 0; /* turn off any debug */
  StopStatisticsThread();
#if HAVE_NDMP
  if (me->ndmp_enable) { StopNdmpThreadServer(); }
#endif
  StopSocketServer();

  StopWatchdog();

  if (sig == SIGTERM) { /* normal shutdown request? */
    /*
     * This is a normal shutdown request. We wiffle through
     *   all open jobs canceling them and trying to wake
     *   them up so that they will report back the correct
     *   volume status.
     */
    foreach_jcr (jcr) {
      SusanSocket* fd;
      if (jcr->JobId == 0) { continue; /* ignore console */ }
      jcr->setJobStatus(JS_Canceled);
      fd = jcr->file_bsock;
      if (fd) {
        fd->SetTimedOut();
        jcr->MyThreadSendSignal(TIMEOUT_SIGNAL);
        Dmsg1(100, "term_stored killing JobId=%d\n", jcr->JobId);
        /* ***FIXME*** wiffle through all dcrs */
        if (jcr->impl->dcr && jcr->impl->dcr->dev &&
            jcr->impl->dcr->dev->blocked()) {
          pthread_cond_broadcast(&jcr->impl->dcr->dev->wait_next_vol);
          Dmsg1(100, "JobId=%u broadcast wait_device_release\n",
                (uint32_t)jcr->JobId);
          ReleaseDeviceCond();
        }
        if (jcr->impl->read_dcr && jcr->impl->read_dcr->dev &&
            jcr->impl->read_dcr->dev->blocked()) {
          pthread_cond_broadcast(&jcr->impl->read_dcr->dev->wait_next_vol);
          Dmsg1(100, "JobId=%u broadcast wait_device_release\n",
                (uint32_t)jcr->JobId);
          ReleaseDeviceCond();
        }
        Bmicrosleep(0, 50000);
      }
      FreeJcr(jcr);
    }
    Bmicrosleep(0, 500000); /* give them 1/2 sec to clean up */
  }

  WriteStateFile(me->working_directory, "susan-sd",
                 GetFirstPortHostOrder(me->SDaddrs));
  DeletePidFile(me->pid_directory, "susan-sd",
                GetFirstPortHostOrder(me->SDaddrs));

  Dmsg1(200, "In TerminateStored() sig=%d\n", sig);

  UnloadSdPlugins();
  FlushCryptoCache();
  FreeVolumeLists();

  foreach_res (device_resource, R_DEVICE) {
    Dmsg1(10, "Term device %s\n", device_resource->device_name);
    if (device_resource->dev) {
      device_resource->dev->ClearVolhdr();
      delete device_resource->dev;
      device_resource->dev = nullptr;
    } else {
      Dmsg1(10, "No dev structure %s\n", device_resource->device_name);
    }
  }

#if defined(HAVE_DYNAMIC_SD_BACKENDS)
  FlushAndCloseBackendDevices();
#endif

  if (configfile) {
    free(configfile);
    configfile = NULL;
  }
  if (my_config) {
    delete my_config;
    my_config = NULL;
  }

  if (debug_level > 10) { PrintMemoryPoolStats(); }
  TermMsg();
  CleanupCrypto();
  TermReservationsLock();
  CloseMemoryPool();

  exit(sig);
}

} /* namespace storagedaemon */
